How Qutebrowser Modes works
As a browser similar to Vim, qutebrowser also defines various modes internally. For instance, there's a hint mode used for clicking links via the keyboard, and a command mode used for inputting commands.
KeyMode
KeyMode
is an enumeration defining all supported modes:
class KeyMode(enum.Enum):
"""Key input modes."""
normal = enum.auto() #: Normal mode (no mode was entered)
hint = enum.auto() #: Hint mode (showing labels for links)
command = enum.auto() #: Command mode (after pressing the colon key)
yesno = enum.auto() #: Yes/No prompts
prompt = enum.auto() #: Text prompts
insert = enum.auto() #: Insert mode (passing through most keys)
passthrough = enum.auto() #: Passthrough mode (passing through all keys)
caret = enum.auto() #: Caret mode (moving cursor with keys)
set_mark = enum.auto()
jump_mark = enum.auto()
record_macro = enum.auto()
run_macro = enum.auto()
# 'register' is a bit of an oddball here: It's not really a "real" mode,
# but it's used in the config for common bindings for
# set_mark/jump_mark/record_macro/run_macro.
register = enum.auto()
ModeManager
The ModeManager class is used to manage the modes of qutebrowser. For detailed information about ModeManager, you can click the link to browse within the notes.
The ModeManager corresponds one-to-one with a MainWindow instance. Its working principle, in brief, involves the following steps:
-
Receiving keyboard events: When a user presses a key, Qt emits a keyboard event. In qutebrowser, the EventFilter receives this event and forwards it to the handle_event method of the currently active (window's) ModeManager.
-
Look up the table to find the corresponding mode handler: Depending on the type of keyboard event (
KeyPress
,KeyRelease
,ShortcutOverride
), it calls different methods of the ModeManager to handle it:_handle_keypress
,_handle_keyrelease
,_handle_keypress
. In_handle_keypress
, it retrieves the current mode and its parser and then calls the parser's handle method to deal with the event.
Each mode has a corresponding parser declared in the init global method of modeman.py.
keyparsers: ParserDictType = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
//...
usertypes.KeyMode.insert:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.insert,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
}
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
Within it, NormalKeyParser
, CommandKeyParser
, and a series of other Parsers are where the specific logic for each mode is implemented.
BaseKeyParser
BaseKeyParser is the base class for various mode KeyParsers. It's a parser for vim-like key sequences and shortcuts.
BaseKeyParser._read_config
Each mode pre-defines a keymap, declared in the Config file. The BaseKeyParser._read_config
method is called by the constructor and is responsible for loading the keymap for the corresponding mode:
def _read_config(self) -> None:
"""Read the configuration."""
self.bindings = BindingTrie()
# read from config
config_bindings = config.key_instance.get_bindings_for(self._mode.name)
for key, cmd in config_bindings.items():
assert cmd
self.bindings[key] = cmd
Taking the default configuration qutebrowser/config/configdata.yml
as an example, here's a part of the keymap:
bindings.default:
no_autoconfig: true
default:
normal:
<Escape>: clear-keychain ;; search ;; fullscreen --leave
o: cmd-set-text -s :open
go: cmd-set-text :open {url:pretty}
O: cmd-set-text -s :open -t
gO: cmd-set-text :open -t -r {url:pretty}
xo: cmd-set-text -s :open -b
xO: cmd-set-text :open -b -r {url:pretty}
wo: cmd-set-text -s :open -w
wO: cmd-set-text :open -w {url:pretty}
/: cmd-set-text /
?: cmd-set-text ?
":": "cmd-set-text :"
ga: open -t
<Ctrl-T>: open -t
<Ctrl-N>: open -w
<Ctrl-Shift-N>: open -p
This shows that the part before the colon is the object to match with, and the part after the colon is the command to execute.
BaseKeyParser.handle
This is the most fundamental keyboard event handling function of KeyParser.
def handle(self, e: QKeyEvent, *,
dry_run: bool = False) -> QKeySequence.SequenceMatch:
"""Handle a new keypress.
"""
# get sequence combine with previous and this event
try:
sequence = self._sequence.append_event(e)
except keyutils.KeyParseError as ex:
# ...
return QKeySequence.SequenceMatch.NoMatch
# if sequence matches a pre defined binding
result = self._match_key(sequence)
del sequence # Enforce code below to use the modified result.sequence
if result.match_type == QKeySequence.SequenceMatch.NoMatch:
result = self._match_without_modifiers(result.sequence)
if result.match_type == QKeySequence.SequenceMatch.NoMatch:
result = self._match_key_mapping(result.sequence)
if result.match_type == QKeySequence.SequenceMatch.NoMatch:
was_count = self._match_count(result.sequence, dry_run)
if was_count:
return QKeySequence.SequenceMatch.ExactMatch
if dry_run:
return result.match_type
self._sequence = result.sequence
self._handle_result(info, result)
return result.match_type
In this method:
-
It first combines the current keyboard event with previous keyboard events to form a sequence.
-
The function calls
self._match_key(sequence)
to match the key sequence. -
If no match is found, the function continues to attempt to match the sequence without modifiers using
self._match_without_modifiers(result.sequence)
. -
If still unmatched, the function tries to match the key mapping using
self._match_key_mapping(result.sequence)
. -
If it still finds no match, the function attempts to match a counter using
self._match_count(result.sequence, dry_run)
.
Here, dry_run
is a boolean parameter used to indicate whether to just check for a match without performing any actions. When dry_run
is True
, the function will return the type of match result without updating the current key sequence or executing any commands.
BaseKeyParser._match_key
def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult:
"""Try to match a given keystring with any bound keychain.
"""
assert sequence
return self.bindings.matches(sequence)
self.bindings
is of type BindingTrie, created in the constructor. Through BindingTrie, it is possible to determine whether the sequence is a NoMatch
, ExactMatch
, or PartialMatch
.
BaseKeyParser._handle_result
The match result is passed into the _handle_result
method to determine whether a command needs to be executed.
def _handle_result(self, info: keyutils.KeyInfo, result: MatchResult) -> None:
"""Handle a final MatchResult from handle()."""
if result.match_type == QKeySequence.SequenceMatch.ExactMatch:
# ...
self.clear_keystring()
self.execute(result.command, count)
elif result.match_type == QKeySequence.SequenceMatch.PartialMatch:
self._debug_log("No match for '{}' (added {})".format(
result.sequence, info))
self.keystring_updated.emit(self._count + str(result.sequence))
elif result.match_type == QKeySequence.SequenceMatch.NoMatch:
self._debug_log("Giving up with '{}', no matches".format(
result.sequence))
self.clear_keystring()
else:
raise utils.Unreachable("Invalid match value {!r}".format(
result.match_type))
If there is an exact match, it calls execute
. The execute
method is implemented by subclasses and is responsible for the execution of the specific command.
CommandKeyParser
The CommandKeyParser is a subclass of BaseKeyParser. In the previous section, we saw that the parser for insert
mode is CommandKeyParser, while the parser for normal
mode, NormalKeyParser, is evidently a subclass of CommandKeyParser.
In this section, we'll first introduce the CommandKeyParser, and in the next section, we'll delve into the NormalKeyParser.
CommandRunner
The most distinctive feature of CommandKeyParser is that it includes the qutebrowser CommandRunner as a member variable. When a sequence matches a command, it directly runs through the CommandRunner.
def __init__(self, *, mode: usertypes.KeyMode,
win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None,
do_log: bool = True,
passthrough: bool = False,
supports_count: bool = True) -> None:
super().__init__(mode=mode, win_id=win_id, parent=parent,
do_log=do_log, passthrough=passthrough,
supports_count=supports_count)
# CommandRunner as member
self._commandrunner = commandrunner
CommandKeyParser.execute
Evidently, CommandKeyParser only needs to pass the sequence into CommandRunner for execution:
def execute(self, cmdstr: str, count: int = None) -> None:
try:
self._commandrunner.run(cmdstr, count)
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())
NormalKeyParser
NormalKeyParser is the corresponding KeyParser for normal
mode, and its most distinctive feature is the addition of two timers.
class NormalKeyParser(CommandKeyParser):
"""KeyParser for normal mode with added STARTCHARS detection and more.
"""
_sequence: keyutils.KeySequence
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(mode=usertypes.KeyMode.normal, win_id=win_id,
commandrunner=commandrunner, parent=parent)
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
self._partial_timer.timeout.connect(self._clear_partial_match)
self._inhibited = False
self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
self._inhibited_timer.setSingleShot(True)
self._inhibited_timer.timeout.connect(self._clear_inhibited)
def __repr__(self) -> str:
return utils.get_repr(self)
def handle(self, e: QKeyEvent, *,
dry_run: bool = False) -> QKeySequence.SequenceMatch:
"""Override to abort if the key is a startchar."""
txt = e.text().strip()
if self._inhibited:
self._debug_log("Ignoring key '{}', because the normal mode is "
"currently inhibited.".format(txt))
return QKeySequence.SequenceMatch.NoMatch
match = super().handle(e, dry_run=dry_run)
if match == QKeySequence.SequenceMatch.PartialMatch and not dry_run:
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
self._partial_timer.start()
return match
def set_inhibited_timeout(self, timeout: int) -> None:
"""Ignore keypresses for the given duration."""
if timeout != 0:
self._debug_log("Inhibiting the normal mode for {}ms.".format(
timeout))
self._inhibited = True
self._inhibited_timer.setInterval(timeout)
self._inhibited_timer.start()
@pyqtSlot()
def _clear_partial_match(self) -> None:
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._sequence))
self._sequence = keyutils.KeySequence()
self.keystring_updated.emit(str(self._sequence))
@pyqtSlot()
def _clear_inhibited(self) -> None:
"""Reset inhibition state after a timeout."""
self._debug_log("Releasing inhibition state of normal mode.")
self._inhibited = False
-
_partial_timer
Timer: This timer is used to clear partial key sequences in cases of partial matches. In thehandle
method, if a key sequence only partially matches (QKeySequence.SequenceMatch.PartialMatch
) anddry_run
isFalse
, the_partial_timer
is started. The timer's interval is determined by theinput.partial_timeout
setting in the configuration file. When the timer times out, it triggers the_clear_partial_match
method, which clears the partially matched key sequence, allowing the user to start entering a new key sequence. -
_inhibited_timer
Timer: This timer is used to reset the_inhibited
state after a certain period, allowing key events to be accepted again. In theset_inhibited_timeout
method, if the passedtimeout
parameter is not zero, the_inhibited_timer
is started. The timer's interval is determined by thetimeout
parameter. When the timer times out, it triggers the_clear_inhibited
method, which resets the_inhibited
state to False, allowing key events to be accepted again. This feature can be used to temporarily inhibit key event processing during specific operations or time periods.
By utilizing these two timers, the NormalKeyParser
can implement functions to clear partial key sequences and reset states under specific conditions. This helps control the logic of key event processing and provides a better user experience.
Conclusion
There are other mode-specific KeyParsers in qutebrowser, which are not covered individually here. Future articles might introduce them separately.
This article has outlined how modes work in qutebrowser, focusing on the underlying logic. There are also corresponding UI elements that interact with these modes, which were not covered in this discussion.
本文作者:Maeiee
本文链接:How Qutebrowser Modes works
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!